﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;

namespace seqdiag
{
    public class SequenceDiagram
    {
        public CallTree Tree;
        public static Font DefaultFont = new Font("Dina", 9);
        public static Font MessageFont = DefaultFont;
        public const float STEP_HEIGHT = 25;
        public const float STEP_HEIGHT_HALF = STEP_HEIGHT / 2;

        private SequenceDiagram(CallTree tree)
        {
            Tree = tree.Root;
        }

        public static SequenceDiagram FromFile(string file)
        {
            string[] lines = File.ReadAllLines(file);
            return new SequenceDiagram(CallTree.FromLines(lines));
        }

        public void Save(string file)
        {
            Bitmap surface = Render(Tree);
            surface.Save(file, ImageFormat.Png);
        }

        private Bitmap Render(CallTree tree)
        {
            Bitmap newImage = new Bitmap(2048, 2048);
            Graphics surface = Graphics.FromImage(newImage);
            surface.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.Default;
            surface.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;

            float x = 0f;
            float y = 75f;
            SortedList<string, CallRenderData> renderedClasses = new SortedList<string, CallRenderData>();
            Render(tree, surface, ref x, ref y, renderedClasses);

            float maxX = 0;
            foreach (CallRenderData callRenderData in renderedClasses.Values)
            {
                float testValue = callRenderData.LeftX + (callRenderData.CenterX - callRenderData.LeftX) * 2;
                if (testValue > maxX)
                {
                    maxX = testValue;
                }
                testValue = callRenderData.TextEndX;
                if (testValue > maxX)
                {
                    maxX = testValue;
                }
            }
            Bitmap croppedImage = new Bitmap(Convert.ToInt32(maxX) + 30, Convert.ToInt32(y) + 30);
            Graphics croppedSurface = Graphics.FromImage(croppedImage);
            croppedSurface.DrawImageUnscaledAndClipped(newImage, new Rectangle(15, 15, croppedImage.Width - 15, croppedImage.Height - 15));

            return croppedImage;
        }

        private void Render(CallTree tree, Graphics surface, ref float currentX, ref float currentY, SortedList<string, CallRenderData> renderedClasses)
        {
            bool renderClass = !renderedClasses.ContainsKey(tree.ClassName);

            float backupX = currentX;
            float backupY = currentY;
            SizeF textSize = surface.MeasureString(tree.ClassName, DefaultFont);

            if (renderClass)
            {
                float widthOfBox = textSize.Width + 2 * 15;
                float leftOfText = currentX + 15;
                float topOfText = 0 + (40 - textSize.Height) / 2;

                surface.DrawRectangle(Pens.Black, currentX, 0, widthOfBox, 40);
                surface.DrawString(tree.ClassName, DefaultFont, Brushes.Black, leftOfText, topOfText);

                float y = 41f;
                float x = currentX + widthOfBox / 2;
                while (y < 2048f)
                {
                    surface.DrawLine(Pens.DarkGray, x, y, x, y + 4);
                    y += 8;
                }

                CallRenderData newRenderData = new CallRenderData(currentX, currentX + widthOfBox / 2, leftOfText + textSize.Width);
                renderedClasses.Add(tree.ClassName, newRenderData);

                currentX += widthOfBox + 50;
            }

            CallRenderData selfRenderData = renderedClasses[tree.ClassName];

            CallTree consumer = tree.IsRoot ? null : tree.Parent;
            CallRenderData consumerRenderData = null;

            bool isSelf = false;
            bool isBackwards = false;
            float backwards = 1;
            float yExtension = 0f;
            float textX = 0f;
            float textY = 0f;
            textSize = surface.MeasureString(tree.MethodName, MessageFont);

            if (consumer != null)
            {
                consumerRenderData = renderedClasses[consumer.ClassName];
                isBackwards = consumerRenderData.LeftX > selfRenderData.LeftX;
                isSelf = consumerRenderData == selfRenderData;
                backwards = isSelf || isBackwards ? -1 : 1;
                yExtension = isSelf ? STEP_HEIGHT_HALF : 0f;

                if (isSelf)
                {
                    surface.DrawArc(Pens.Black, selfRenderData.CenterX - STEP_HEIGHT, currentY - yExtension, STEP_HEIGHT * 2, STEP_HEIGHT, 270, 180);
                    textY = currentY - textSize.Height / 2 - 1;
                    textX = selfRenderData.CenterX + STEP_HEIGHT;
                }
                else
                {
                    surface.DrawLine(Pens.Black, consumerRenderData.CenterX, currentY, selfRenderData.CenterX, currentY);
                    textY = currentY - textSize.Height;
                    textX = (isBackwards ? selfRenderData : consumerRenderData).CenterX + (Math.Abs(consumerRenderData.CenterX - selfRenderData.CenterX) - textSize.Width) / 2;
                }

                surface.FillPolygon
                (
                    Brushes.Black,
                    new PointF[]
                    {
                        new PointF(selfRenderData.CenterX, currentY + yExtension), 
                        new PointF(selfRenderData.CenterX - 5 * backwards, currentY + yExtension + 5), 
                        new PointF(selfRenderData.CenterX - 5 * backwards, currentY + yExtension - 5)
                    }
                );

                surface.DrawString(tree.MethodName, MessageFont, Brushes.Black, textX, textY);

                if (selfRenderData.TextEndX < textX + textSize.Width)
                {
                    selfRenderData.TextEndX = textX + textSize.Width;
                }

                currentY += STEP_HEIGHT;
            }

            foreach (CallTree child in tree.Children)
            {
                Render(child, surface, ref currentX, ref currentY, renderedClasses);
            }

            if (consumer != null && consumerRenderData != null)
            {
                string returnValueString = string.IsNullOrEmpty(tree.ReturnValue) ? "void::" + tree.MethodName : tree.ReturnValue;

                textSize = surface.MeasureString(returnValueString, DefaultFont);

                if (isSelf)
                {
                    surface.DrawArc(Pens.Gray, selfRenderData.CenterX - STEP_HEIGHT, currentY - yExtension, STEP_HEIGHT * 2, STEP_HEIGHT, 90, 180);
                    textY = currentY - textSize.Height / 2 - 1;
                    textX = selfRenderData.CenterX - STEP_HEIGHT_HALF;
                }
                else
                {
                    textY = currentY - textSize.Height;
                    textX = (isBackwards ? selfRenderData : consumerRenderData).CenterX + (Math.Abs(consumerRenderData.CenterX - selfRenderData.CenterX) - textSize.Width) / 2;

                    surface.DrawLine(Pens.Gray, selfRenderData.CenterX, currentY, consumerRenderData.CenterX, currentY);
                }

                surface.FillPolygon
                (
                    Brushes.Gray,
                    new PointF[]
                    {
                        new PointF(consumerRenderData.CenterX, currentY + yExtension), 
                        new PointF(consumerRenderData.CenterX - 5 * -backwards, currentY + yExtension + 5), 
                        new PointF(consumerRenderData.CenterX - 5 * -backwards, currentY + yExtension - 5)
                    }
                );

                surface.DrawString(returnValueString, MessageFont, Brushes.Gray, textX, textY);

                if (selfRenderData.TextEndX < textX + textSize.Width)
                {
                    selfRenderData.TextEndX = textX + textSize.Width;
                }

                currentY += STEP_HEIGHT;
            }
        }
    }
}
